package be.destin.memolog;

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataObjectUtils;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.access.DataContext;
import org.apache.cayenne.configuration.CayenneRuntime;
import org.apache.cayenne.configuration.web.WebUtil;
import org.apache.cayenne.query.ObjectIdQuery;
import org.apache.cayenne.query.SQLTemplate;
import org.apache.cayenne.query.SelectQuery;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.log4j.Logger;

import be.destin.cayenne.CayenneHelpers;
import be.destin.memo.Comment;
import be.destin.memo.Detail;
import be.destin.memo.DisplayLog;
import be.destin.memo.Log;
import be.destin.memo.ReadLog;
import be.destin.memo.SearchLog;
import be.destin.memo.TagDef;
import be.destin.memo.Tagged;
import be.destin.memo.UpdateLog;

/*
 * TODO: Tags doit suivre la m�me logique que Logs, quitte � ajouter un champ "tag" � la classe Log...
 * Ceci veut dire aussi une s�paration entre Tags et TagsDetails?!
 */

public class UsageLogger {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	long internalId = 0;
	long cpu=0;
	String application="";
	String user=null;
	String record="";
	String cachedTitle="";
	String infoMessage=null;
	UsageMode mode = null; // null signifie "donn�es synth�tiques pour Atom Feed"
	Log[] loggerRec = null;
	HttpServletRequest request = null;
	ObjectContext context = null;
	long count = -1;
	Timestamp now = new Timestamp(System.currentTimeMillis());
	TagDef tagHead = null;

	static Logger log = Logger.getLogger(UsageLogger.class);

	static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //1970-01-01 00:00:00
	/*
	 * Example:
	UsageLogger usageLogger = UsageLogger.getInstance(application) ;
	�
	usageLogger.start(httpServletRequest, CONSULT|READ|UPDATE|SEARCH, record) ;
	ou usageLogger.start(userID, CONSULT|READ|UPDATE|SEARCH, record) ;
	�
	usageLogger.info(� message added in the Info field �) ; (can be repeated) 
	�
	usageLogger.stop() ;
	�
	usageLogger = usageLogger.releaseInstance() ; 
	 */

	/**
	 * This must be called first to get a "logger" for current action.
	 * The applicationId
	 * @param applicationId  is necessary to sort out actions in the database from different applications.
	 * @return a UsageLogger for current request.
	 */
	public UsageLogger (HttpServletRequest request, String applicationId, String recordId, UsageMode mode) {
		this.request = request;
		this.application = applicationId;
		this.record = recordId;
		this.mode = mode;
	}

	public UsageLogger (String user, String applicationId, String recordId, UsageMode mode) {
		this.user = user;
		this.application = applicationId;
		this.record = recordId;
		this.mode = mode;
	}
	
	public UsageLogger(UsageLogger template, UsageMode mode) {
		this.request = template.request;
		this.context = template.context;
		this.user = template.user;
		this.application = template.application;
		this.record = template.record;
		this.mode = mode;
	}

	public UsageLogger(UsageLogger base, Log entity) {
		this.mode = UsageMode.findLogClass(entity);
		this.application = entity.getAppli();
		this.record = entity.getRecord();
		this.user = entity.getUser();
		this.request = base.request;
		this.context = base.context;
	}

	public UsageLogger (String id) {
		String toParse = id;
/*
		int pTime = toParse.indexOf(' ');
		if (pTime < 0) return null;
		this.mode = toParse.substring(0,pTime);
		toParse = toParse.substring(pTime+1);

		int pRepeat = toParse.indexOf('x');
		if (pRepeat < 0) return null;
		String timeStamp = toParse.substring(0,pRepeat);
		toParse = toParse.substring(pRepeat+1);

		int pUsr = toParse.indexOf(' ');
		if (pUsr < 0) return null;
		String repetition = toParse.substring(0,pUsr);
		toParse = toParse.substring(pUsr+1);
*/
		int pApp = toParse.indexOf(':');
		if (pApp < 0) return;
		this.user = toParse.substring(0,pApp);
		toParse = toParse.substring(pApp+1);

		int pRec = toParse.indexOf('/');
		if (pRec < 0) return;
		this.application = toParse.substring(0,pRec).trim();
		toParse = toParse.substring(pRec+1);

		this.record = toParse.substring(pApp).trim();
	}

	public String getUserApplicationRecord() {
		return this.getUser()+':'+this.application+'/'+this.record;
	}

	/**
	 * This starts the logging of an action.
	 * @param currRequest: Web request. NULL for batch mode.
	 * @param aMode: Type of action (see UsageMode) 
	 * @param recordId: string which can be used as a key to retrieve a record for the current application.
	 */
	public void start () {
		this.cpu = System.currentTimeMillis();
	}

	public UsageCount count() {
		return eval(this.getContext(),this.getUser(),mode,record);
	}

	UsageCount eval(ObjectContext context, String user, UsageMode aMode, String recordId) {
		if (context == null) return null;
		if (CayenneHelpers.lastError != 0) return null;
		
		
		
		SQLTemplate query = null;
		try {
			query = (SQLTemplate) context.getEntityResolver().lookupQuery(aMode.getNiceName()+"CountQuery");
			Map<String,String> params = new HashMap<String,String>();
			params.put("appli", application);
			//params.put("user", user)
			params.put("record", recordId);
			query = query.queryWithParameters(params);
			List<DataRow> records = context.performQuery(query);
			UsageCount total = new UsageCount();
			for (DataRow dr : records) {
				String owner = (String) dr.get("user");
				 //get our "count" variable back that we "selected INTO" in our sql string.
				if (owner != null && owner.equalsIgnoreCase(user)) {
					total.setMines(total.getMines()+(Long) dr.get("count"));
				} else {
					total.setTheirs(total.getTheirs()+(Long) dr.get("count"));					
				}
				Date date = null;
				Object tf = dr.get("timefirst");
				if (tf instanceof java.sql.Timestamp) {
					Timestamp aTimestamp = (java.sql.Timestamp) tf;
					date = new Date(aTimestamp.getTime());
				} else if (tf instanceof Date) {
					date = (Date) tf;
				}
				if (date.compareTo(total.getMinDate()) < 0) total.setMinDate(date);
				date = null;
				tf = dr.get("timelast");
				if (tf instanceof java.sql.Timestamp) {
					Timestamp aTimestamp = (java.sql.Timestamp) tf;
					date = new Date(aTimestamp.getTime());
				} else if (tf instanceof Date) {
					date = (Date) tf;
				}

				if (date.compareTo(total.getMaxDate()) > 0) total.setMaxDate(date);
			}
			return total;
		} catch (CayenneRuntimeException cre) {
			log.error("counting appli="+application+", record="+recordId+", mode="+aMode.name()+", query="+CayenneHelpers.xmlDump(query), cre );
			CayenneHelpers.notOK();
			return null; // no log possible
		}
	}

	public long[] total() {
		if (CayenneHelpers.lastError != 0) return null;
		if (getContext()==null) return null;
		SQLTemplate query = null;
		try {
			query = (SQLTemplate) context.getEntityResolver().lookupQuery(this.mode.getNiceName()+"ListQuery");
			Map<String,String> params = new HashMap<String,String>();
			params.put("appli", application);
			params.put("user", user);
			query = query.queryWithParameters(params);
			List<DataRow> records = context.performQuery(query);
			long total = 0;
			long nbRec = 0;
			for (DataRow dr : records) {
				total += (Long) dr.get("count");  //get our "count" variable back that we "selected INTO" in our sql string.
				nbRec++;
			}
			long[] result = new long[2];
			result[0] = nbRec;
			result[1] = total;
			return result;
		} catch (CayenneRuntimeException cre) {
			log.error("appli="+application+", user="+user+", mode="+this.mode.name()+", query="+CayenneHelpers.xmlDump(query), cre );
			CayenneHelpers.notOK();
			return null; // no log possible
		}
	}

	public List<UsageLogger> listUsage() {
		List<UsageLogger> entries = new LinkedList<UsageLogger>();
		if (CayenneHelpers.lastError != 0) return entries;
		if (getContext()==null) return entries;
		SQLTemplate query = null;
		try {
			query = (SQLTemplate) context.getEntityResolver().lookupQuery(this.mode.getNiceName()+"ListQuery");
			Map<String,String> params = new HashMap<String,String>();
			params.put("appli", application);
			params.put("user", user);
			query = query.queryWithParameters(params);
			List<DataRow> records = context.performQuery(query);
			long total = 0;
			for (DataRow dr : records) {
				UsageLogger newLog = new UsageLogger(this,this.mode);
				entries.add(newLog);
				newLog.setCount((Long) dr.get("count"));  //get our "count" variable back that we "selected INTO" in our sql string.
				newLog.setRecord((String)dr.get("record"));
				newLog.setNow((Timestamp)dr.get("timelast"));
				//newLog.setPublished((String)dr.get("timefirst"));
			}
		} catch (CayenneRuntimeException cre) {
			log.error("appli="+application+", user="+user+", mode="+this.mode.name()+", query="+CayenneHelpers.xmlDump(query), cre );
			CayenneHelpers.notOK();
		}
		return entries;
	}

	/**
	 * Record a piece of info about a logged action (UPDATE).
	 * A delimiter (\n) is placed between the different occurrences of "info" 
	 * @param message
	 */
	public void info (String message) {
		if (message == null) return;
		if (infoMessage == null) infoMessage = message;
		else {
			infoMessage = infoMessage+'\n'+message;
		}
	}
	
	public Log create() {
		if (getContext() == null) return null;
		Log newLog = context.newObject(mode.getLogClass());
		newLog.setAppli(application);
		newLog.setUser(user);
		newLog.setRecord(record);
		newLog.setTime(this.now);
		if (mode == UsageMode.TAGGED) {
			((Tagged)newLog).setTaghead(this.tagHead);
		}
		if (mode == UsageMode.COMMENT) {
			((Comment)newLog).setComment(this.getInfoMessage());
		}
		context.commitChanges();		
		this.internalId = newLog.getId();
		return newLog;
	}

	public Log update(Log oldLog) {
		oldLog.setAppli(application);
		oldLog.setUser(user);
		oldLog.setRecord(record);
		oldLog.setTime(this.now);
		if (mode == UsageMode.TAGGED) {
			((Tagged)oldLog).setTaghead(this.tagHead);
		}
		if (mode == UsageMode.COMMENT) {
			((Comment)oldLog).setComment(this.getInfoMessage());
		}
		context.commitChanges();		
		this.internalId = oldLog.getId();
		return oldLog;
	}

	/**
	 * Conclude and record a logging measure.
	 * Uses the database to record the data...
	 */
	public synchronized Log stop() {
		Log[] logs = getLoggerRec();
		if (!CayenneHelpers.isOK()) return null;
		Log newLog = null;
		if (logs != null && logs.length > 0) newLog = logs[0];

		SelectQuery query = null;
		try {
			if (newLog == null) {
				newLog = this.create();
			} else {
				this.internalId = newLog.getId();
			}

			if (mode == UsageMode.TAGDEF || mode == UsageMode.TAGGED) return newLog; // DO NOT CREATE a "Detail" record!

			Map<String,Object> params2 = new HashMap<String,Object>();
			params2.put("logId", this.internalId);
			Timestamp stamp;
			if (mode == UsageMode.DISPLAY || mode == UsageMode.READ) { // Precision of ONE day
				stamp = new Timestamp(DateUtils.truncate(this.now, Calendar.DATE).getTime());
			} else if (mode == UsageMode.SEARCH) { // Full time precision for the stamp but only one per day
				stamp = new Timestamp(DateUtils.truncate(this.now, Calendar.DATE).getTime());
				this.info(sdf.format(this.now));
			} else { // UPDATE
				stamp = this.now;
			}
			params2.put("time", stamp);

			ObjectId detailId = new ObjectId(mode.getNiceName()+"Detail",params2); 
			ObjectIdQuery query2 = new ObjectIdQuery(detailId);
			Detail newDetail = (Detail) DataObjectUtils.objectForQuery(context, query2);
			if (newDetail == null) {
				newDetail = context.newObject(mode.getDetailClass());
				newDetail.setLogId(newLog.getId());
				if (mode != UsageMode.UPDATE) {
					newDetail.setTime(stamp);
					newDetail.setInfo(infoMessage);
					if (cpu == 0) cpu = now.getTime(); // In case start has not been done
					cpu = System.currentTimeMillis() - cpu;
					newDetail.setCpu(cpu);
					context.commitChanges();
				}
			}
			return newLog;
		} catch (CayenneRuntimeException cre) {
			CayenneHelpers.notOK();
			log.error("appli="+application+", record="+record+", user="+user+", mode="+mode.name()+", query="+CayenneHelpers.xmlDump(query), cre );
			return null; // no log possible
		}
	}

	/**
	 * Conclude and record a logging measure.
	 * Uses the database to record the data...
	 */
	public synchronized void delete() {
		Log[] logs = getLoggerRec();
		if (!CayenneHelpers.isOK()) return;
		if (logs == null) return;

		SelectQuery query = null;
		try {
			for (Log newLog : logs) {
				switch ( mode ) {
					case DISPLAY: {
						context.deleteObjects(((DisplayLog)newLog).getDaily());
						break;
					}
					case READ: {
						context.deleteObjects(((ReadLog)newLog).getDaily());
						break;
					}
					case SEARCH: {
						context.deleteObjects(((SearchLog)newLog).getDaily());
						break;
						
					}
					case UPDATE: {
						context.deleteObjects(((UpdateLog)newLog).getLogDetail());
						break;
					}
					case COMMENT: {
						// No subordinated data
						break;
					}
					case TAGDEF: {
						context.deleteObjects(((TagDef)newLog).getTagged());
						break;
					}
					case TAGGED: {
						// No subordinated data
						break;
					}
				}
				context.deleteObject(newLog);
			}
		} catch (CayenneRuntimeException cre) {
			CayenneHelpers.notOK();
			log.error("deleting appli="+application+", record="+record+", user="+user+", mode="+mode.name()+", query="+CayenneHelpers.xmlDump(query), cre );
			return; // no log possible
		}
	}

	public String getApplication() {
		return application;
	}

	public void setApplication(String application) {
		this.application = application;
	}

	public String getUser() {
		if (user == null) {
			if (request != null) {
				user = request.getRemoteUser();
				if (user == null) {
					user = "UNKNOWN";
				}
			} else {
				user = "BATCH";
			}				
		}
		return user;
	}

	public ObjectContext getContext() {
		if (context != null) return context;
		if (!CayenneHelpers.isOK()) return null;
		if (request != null) {
			ServletContext servletContext = request.getServletContext();
			CayenneRuntime runtime = WebUtil.getCayenneRuntime(servletContext);
			if (runtime != null) {
				context = runtime.getContext();
			}
		}
		if (context == null) {
			try {
				context = DataContext.getThreadObjectContext();
			} catch (IllegalStateException e) {
				CayenneHelpers.notOK();
				log.error("appli="+application+", record="+record+", user="+user+", mode="+mode.name(), e );
				return null;
			}
		}
		return context;
	}
	
	public void setContext(ObjectContext context) {
		this.context = context;
	}

	public void setUser(String user) {
		this.user = user;
	}

	public String getRecord() {
		return record;
	}

	public void setRecord(String record) {
		this.record = record;
	}

	public String getInfoMessage() {
		return infoMessage;
	}

	public void setInfoMessage(String infoMessage) {
		this.infoMessage = infoMessage;
	}

	/**
	 * @return the cachedTitle
	 */
	public String getCachedTitle() {
		return cachedTitle;
	}

	/**
	 * @param cachedTitle the cachedTitle to set
	 */
	public void setCachedTitle(String cachedTitle) {
		this.cachedTitle = cachedTitle;
	}

	public UsageMode getMode() {
		return mode;
	}

	public void setMode(UsageMode mode) {
		this.mode = mode;
	}

	public Log[] getLoggerRec() {
		if (loggerRec != null) return loggerRec;
		if (getContext() == null) return null;
		if (!CayenneHelpers.isOK()) return null;
		SelectQuery query = null;
		try {
			query = (SelectQuery) context.getEntityResolver().lookupQuery(mode.getNiceName()+"LogExist");
			Map<String,String> params = new HashMap<String,String>();
			params.put("appli", application);
			params.put("user", this.getUser());
			params.put("record", record);
			query = query.queryWithParameters(params);

			List<Log> logs = context.performQuery(query);
			if (logs.size() >= 0) {
				Log[] result = new Log[0];
				loggerRec = logs.toArray(result);
			} else return null;
		} catch (CayenneRuntimeException cre) {
			CayenneHelpers.notOK();
			log.error("appli="+application+", record="+record+", user="+user+", mode="+mode.name()+", query="+CayenneHelpers.xmlDump(query), cre );
			return null; // no log possible
		}
		return loggerRec;
	}

	public Log[] getAllLoggerRec() {
		if (getContext() == null) return null;
		if (!CayenneHelpers.isOK()) return null;
		SelectQuery query = null;
		try {
			query = (SelectQuery) context.getEntityResolver().lookupQuery(mode.getNiceName()+"LogEverybody");
			Map<String,String> params = new HashMap<String,String>();
			params.put("appli", application);
			//params.put("user", this.getUser());
			params.put("record", record);
			query = query.queryWithParameters(params);

			List<Log> logs = context.performQuery(query);
			if (logs.size() >= 0) {
				Log[] result = new Log[0];
				return logs.toArray(result);
			} else return null;
		} catch (CayenneRuntimeException cre) {
			CayenneHelpers.notOK();
			log.error("appli="+application+", record="+record+", user="+user+", mode="+mode.name()+", query="+CayenneHelpers.xmlDump(query), cre );
			return null; // no log possible
		}
	}

	public void setLoggerRec(Log[] loggerRec) {
		this.loggerRec = loggerRec;
	}

	public long getCpu() {
		return cpu;
	}

	public void setCpu(long cpu) {
		this.cpu = cpu;
	}

	public HttpServletRequest getRequest() {
		return request;
	}

	public void setRequest(HttpServletRequest request) {
		this.request = request;
	}

	public long getCount() {
		return count;
	}

	public void setCount(long count) {
		this.count = count;
	}

	public long getInternalId() {
		if (this.internalId > 0) return this.internalId;
		Log[] existingLogs = this.getLoggerRec();
		if (!CayenneHelpers.isOK()) return 0;
		if (existingLogs != null && existingLogs.length > 0) {
			this.internalId = existingLogs[0].getId();
		}
		return 0;
	}

	public void setInternalId(long internalId) {
		this.internalId = internalId;
	}

	public Timestamp getNow() {
		return now;
	}

	public void setNow(Timestamp aStamp) { // Use with greatest care!
		if (aStamp != null) {
 			this.now = aStamp;
		}
	}

	public TagDef getTagHead() {
		return tagHead;
	}

	public void setTagHead(TagDef tagHead) {
		this.tagHead = tagHead;
	}

}
